package cn.bincker.mybatis.encrypt.reflection;

import cn.bincker.mybatis.encrypt.annotation.Encrypt;
import cn.bincker.mybatis.encrypt.annotation.IntegrityCheck;
import cn.bincker.mybatis.encrypt.entity.EncryptProperty;
import cn.bincker.mybatis.encrypt.entity.IntegrityCheckProperty;
import cn.bincker.mybatis.encrypt.entity.SecurityProperty;
import com.baomidou.mybatisplus.annotation.TableId;
import org.springframework.beans.BeanUtils;

import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class ReflectionUtils {
    public static Map<String, SecurityProperty> getEncryptProperties(Class<?> clazz){
        var result = new HashMap<String, SecurityProperty>();
        var properties = BeanUtils.getPropertyDescriptors(clazz);
        for (PropertyDescriptor property : properties) {
            var method = property.getReadMethod();
            var propertyName = property.getName();
            if (result.containsKey(propertyName)) continue;
            if (propertyName.startsWith("$") || "serialVersionUID".equals(propertyName) || "class".equals(propertyName)) continue;
            Field field = getDeclaredField(clazz, propertyName);
            var encryptAnnotation = getAnnotation(method, field, Encrypt.class);
            if (encryptAnnotation != null) {
                var encryptProperty = new EncryptProperty(clazz, propertyName, method, property.getWriteMethod(), field);
                result.put(propertyName, encryptProperty);
            }
            var integrityCheckAnnotation = getAnnotation(method, field, IntegrityCheck.class);
            if (integrityCheckAnnotation != null) {
                var encryptProperty = new IntegrityCheckProperty(clazz, propertyName, method, field, integrityCheckAnnotation);
                result.put(propertyName, encryptProperty);
            }
        }
        return result;
    }

    private static Field getDeclaredField(Class<?> clazz, String propertyName) {
        var cls = clazz;
        while (cls != null && cls != Object.class){
            try {
                return cls.getDeclaredField(propertyName);
            } catch (NoSuchFieldException ignore) {}
            cls = cls.getSuperclass();
        }
        return null;
    }

    private static <T extends Annotation> T getAnnotation(Method getter, Field field, Class<T> type){
        T result;
        if (getter != null){
            result = getter.getAnnotation(type);
            if (result != null) return result;
        }
        return field != null ? field.getAnnotation(type) : null;
    }

    public static List<Object> getEntityIds(Object entity){
        var cls = ProxyUtils.getTargetClass(entity);
        var ids = Stream.of(cls.getDeclaredFields()).filter(f -> f.getAnnotation(TableId.class) != null)
                .map(f-> {
                    try {
                        var pd = BeanUtils.getPropertyDescriptor(cls, f.getName());
                        if (pd != null){
                            return pd.getReadMethod().invoke(entity);
                        }
                        f.setAccessible(false);
                        return f.get(entity);
                    } catch (IllegalAccessException | InvocationTargetException ex) {
                        return null;
                    }
                }).collect(Collectors.toList());
        if (ids.isEmpty()){
            ids = Arrays.stream(BeanUtils.getPropertyDescriptors(cls)).filter(p->p.getReadMethod().getAnnotation(TableId.class) != null)
                    .map(p-> {
                        try {
                            p.getReadMethod().setAccessible(false);
                            return p.getReadMethod().invoke(entity);
                        } catch (Throwable ex) {
                            return null;
                        }
                    }).collect(Collectors.toList());
        }
        return ids;
    }
}
